<?php
/**
 * User group class.
 * 
 * LICENSE:
 * 
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with This library. If not, see <{@link http://www.gnu.org/licenses/}>.
 *
 * @link http://code.google.com/p/phpstartapp/
 * @author Bojan Mauser <bmauser@gmail.com>
 * @copyright Bojan Mauser 2009
 * @package psa
 * @version $Id: Psa_Group.class.php 464 2009-03-24 00:30:12Z bmauser $
 */

/**
 * User group class.
 * 
 * Here are some examples of using {@link Psa_Group} object.
 * 
 * <b>Examples</b>
 * 
 * <b>1)</b> Create new user group:
 * <code>
 * // if you want to create new user group use 'new' as argument to constructor
 * $group = new Psa_Group('new');
 * 
 * // set the name for new group
 * $group->name = 'my_new_user_group';
 * 
 * // you can set some custom properties of the new group
 * // and they will be saved with the group
 * $group->is_important_group = true;
 * $group->users_like = array('pizza','fish');
 * 
 * // save group to database and get ID of newly created group
 * $new_group_id = $group->save();
 * </code>
 * 
 * <b>2)</b> Manage group tags:
 * <code>
 * // work with user group named 'my_user_group'
 * $group = new Psa_Group('my_user_group');
 * 
 * // add tag 'soap_controller', 'checkout' and 'download' to group
 * // so all users in this group will have those tags
 * $group->add_tag('soap_controller');
 * $group->add_tag(array('checkout','download'));
 * 
 * // remove tag 'admin'
 * $group->remove_tag('admin');

 * // you have to save group to store tags to database
 * $group->save();
 * </code>
 * 
 * <b>Note:</b> Read more about the tags {@tutorial psa_features.pkg#usersgroups.tags here}.
 */
class Psa_Group{
	
	/**
	 * Group ID.
	 * Primary key from <i>psa_group</i> database table.
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var int
	 */
	protected $id;
	
	
	/**
	 * Name of the group.
	 * There cannot be two groups with the same name.
	 * You will need to set this property when creating a new group or renaming group.
	 * 
	 * @var string
	 */
	public $name;
	
	
	/**
	 * Array with group tags.
	 * Indexes of this array are tag names. This array contains only tags that are assigned to this group.
	 * Manage this array with {@link add_tag()} and {@link remove_tag()} methods.
	 * Read more about tags {@tutorial psa_features.pkg#usersgroups.tags here}.
	 * 
	 * Example:<pre>
	 * Array
	 * (
	 *     ['soap_controller'] => 1  // index is tag id (name). All elements have value 1.
	 *     ['download_image'] => 1  
	 *     ['admin_operations'] => 1  
	 * )
	 * </pre>
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var array
	 * @see add_tag()
	 * @see remove_tag()
	 * {@internal @see add_remove_tag()  }}
	 */
	protected $tags = array();
	
	
	/**
	 * Database connect object.
	 * 
	 * @var Psa_Pdo
	 * @ignore
	 */
	protected $psa_database;
	
	
	/**
	 * Flag if new group creation process is started.
	 * It is set from constructor.
	 * 
	 * @var int
	 * @ignore
	 */
	protected $new_group = 0;
	
	
	/**
	 * Flag if the group exists in the database.
	 * This value is set after group object is instanced or after saving a newly created group. 
	 * You can use it to check if the group exists in the database.
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var int 1-true, 0-false
	 */
	protected $exists = 0;
	
	
	/**
	 * Array that holds valuse of properties from custom columns added to psa_group table.
	 * 
	 * @var array
	 * @ignore 
	 */
	protected $orm_data = array();
	
	
	/**
	 * Array that holds references to values that are changed in $orm_data array.
	 * This is used to decide which orm properties to update in database.
	 * 
	 * @var array
	 * @ignore 
	 */
	protected $orm_data_changed = array();

	

	/**
	 * Constructor.
	 * 
	 * Must be called with group id (integer value) or group name (string value) as argument. 
	 * Pass <kbd>'new'</kbd> (string) argument if you want to create a new group.
	 * If you want to call constructor with group id (integer value) be sure to pass integer
	 * as argument and use cast operator <kbd>(int)</kbd>.
	 * See examples in {@link Psa_Group} class description.
	 * 
	 * @param int|string $group group id or 'new' if new group should be created
	 */
	public function __construct($group){

		// global database connect object
		global $psa_database;
		
		// reference to global database object
		$this->psa_database = $psa_database;
		
		// new group
		if($group === 'new'){
			// flag that new group creation process is started
			$this->new_group = 1;
			// find if we have some orm data
			$this->restore_new_group();
		}
		// existing group
		else if($group && is_int($group)){
			$this->id = $group;
			$this->restore(); // sets group properties (load previously saved data)
		}
		else{
			$this->name = $group;
			$this->restore(); // sets group properties (load previously saved data)
		}
	}


	/**
	 * Restores group properties from database previously saved with {@link save()} method.
	 * This method is called from the constructor.
	 * 
	 * @see save() 
	 * @return int 1 for success, 0 for failure
	 * @throws Psa_Group_Exception
	 * @ignore
	 */
	protected function restore(){

		// if id of the group is set
		if($this->id or $this->name){
			
			// global config array
			global $PSA_CFG;
			
			// load group data from the database
			if(is_int($this->id))
				$sql = "SELECT * FROM {$PSA_CFG['database']['table']['group']} WHERE psa_id = '{$this->id}'";
			else if($this->name)
				$sql = "SELECT * FROM {$PSA_CFG['database']['table']['group']} WHERE psa_name = '{$this->name}'";
				
			// run query against the database
			$this->psa_database->query($sql);
			
			// All data about the group. Entire row from database table
			$group_data = $this->psa_database->fetch_row();
			
			// if row in the database table exists
			if(isset($group_data['psa_id'])){
				
				if($group_data['psa_serialized_data']){
					
					// load all saved properties from saved object in the database
					$serialized_object = unserialize(base64_decode($group_data['psa_serialized_data']));
					
					if(is_object($serialized_object)){
						// unset some properties to not override them in current group object
						$this->serialize_clean_obj($serialized_object);
						
						// set current objects properties
						foreach($serialized_object as $serialized_object_key => $serialized_object_value){
							$this->$serialized_object_key = $serialized_object_value;
						}
					}
				}
				// check if there are some custom table rows in pas_user database table
				if(sizeof($group_data) > 3){
					$this->set_orm_data($group_data);
				}
				
				// set objects properties from databse
				$this->id = (int) $group_data['psa_id'];
				$this->name = $group_data['psa_name'];
				
				// set $exists flag that group exists in the database
				$this->exists = 1;
				
				return 1;
			}
			// group does not exists
			else{
				include_once 'Psa_Group_Exception.class.php';
				throw new Psa_Group_Exception("Group does not exists. Cannot restore data.", 31);
			}
		}
		else{
			
			include_once 'Psa_Group_Exception.class.php';
			throw new Psa_Group_Exception("Group id or group name are not set. Cannot load data about the group", 32);
		}
	}

	
	/**
	 * Checks if there are some custom table rows in pas_group database table.
	 * It finds out all columns from database and fills $orm_data array.
	 * 
	 * @ignore 
	 */
	protected function restore_new_group(){
		
		global $PSA_CFG;
		
		// We need one dummy query here to get all column names.
		$sql_dummy = "SELECT * FROM {$PSA_CFG['database']['table']['group']} LIMIT 1";
		
		$this->psa_database->query($sql_dummy);
		
		// All data about the group. Entire row from database table
		$sql_dummy_group_data = $this->psa_database->fetch_row();
		
		if(sizeof($sql_dummy_group_data) > 3){
			$this->set_orm_data($sql_dummy_group_data,1);
		}
	}
	

	/**
	 * Saves all properties of the group object to the database.
	 * All saved properties will be restored {@internal with restore() method called from constructor}} 
	 * when group object is created.
	 * It also sets {@link $id $id} member variable if a new group is created.
	 * See examples in {@link Psa_Group} class description. This method will throw
	 * {@link Psa_Group_Exception} on error.
	 * 
	 * @return int id of saved group for success
	 * @throws Psa_Group_Exception
	 * @see __construct
	 */
	public function save(){

		if($this->exists or ($this->new_group && $this->name)){
			
			// global config array
			global $PSA_CFG;
			
			// save only relevant data
			$temp_obj = clone $this;
			
			// unset unneeded data. Store only needed data in database.
			$this->serialize_clean_obj($temp_obj);
			
			// serialize object
			$serialized_data = base64_encode(serialize($temp_obj));
			
			// if this is new group creation
			if($this->new_group){
				// check if there are some custom table rows in psa_group database table
				$custom_columns_insert = psa_orm_make_query_parts(2, $this->orm_data_changed);
				$sql = "INSERT INTO {$PSA_CFG['database']['table']['group']} (psa_serialized_data,psa_name" .$custom_columns_insert['insert_query_columns']. ") VALUES (?, ?" . $custom_columns_insert['insert_query_values'] . ')';
				$query_params = array_merge(array($serialized_data, $this->name),$custom_columns_insert['values']);
			}
			// update existing group in the database
			else{
				// check if there are some custom table rows in psa_group database table
				$custom_columns_update = psa_orm_make_query_parts(1, $this->orm_data_changed);
				$sql = "UPDATE {$PSA_CFG['database']['table']['group']} SET psa_serialized_data = ?, psa_name= ?" . $custom_columns_update['update_query_set'] . " WHERE psa_id = ?";
				$query_params = array_merge(array($serialized_data, $this->name),$custom_columns_update['values']);
				$query_params[] = $this->id;
			}
			
			// run Psa_Plugin_Before_group_Create plugins
			if($this->new_group){
				psa_run_plugins(array('Psa_Plugin_Before_group_Create' => array('psa_main' => array($this))),'by_type','no_unregistered_warning');
			}
			
			try{
				// run query against the database
				$this->psa_database->execute($query_params,$this->psa_database->prepare($sql));
				
				// set the id value of object if new group is created
				if($this->new_group){
					
					// set the id of new group
					$this->id = (int) $this->psa_database->last_insert_id('psa_group_id_seq');
					
					// turn off new_group flag
					$this->new_group = 0;
					
					// set $exists flag that group exists in the database
					$this->exists = 1;
					
					// run Psa_Plugin_After_Group_Create plugins
					psa_run_plugins(array('Psa_Plugin_After_Group_Create' => array('psa_main' => array($this))),'by_type','no_unregistered_warning');
					
					// restore all settings to get default orm data
					if(count($this->orm_data))
						$this->restore();
					
					// for log
					$log_message = "New group created";
				}else{
					// for log
					$log_message = "Group data saved";
				}
				
				// all orm properties are saved now so reset changes tracker
				$this->orm_data_changed = array();
				
				// write log
				$this->log($log_message, __METHOD__, 2);
				
				return $this->id;
			}
			catch (Psa_Db_Exception $e){
				include_once 'Psa_Group_Exception.class.php';
				throw new Psa_Group_Exception("Error saving group data to database.", 34);
			}
		}
		else{
			include_once 'Psa_Group_Exception.class.php';
			throw new Psa_Group_Exception("Error saving group data to database. Group not exists or name not set for new group", 35);
		}
	}
	
	
	/**
	 * Fills $this->orm_data and sets values of object properties from orm database columns.
	 * 
	 * @param array $group_data 'SELECT *' row from psa_group table
	 * @param bool $set_nulls if true values in $this->orm_data will be filled with nulls. This is for 
	 * creation of new groups.
	 * @ignore 
	 */
	protected function set_orm_data(&$group_data,$set_nulls = 0){
		
		foreach ($group_data as $group_data_key => $group_data_value) {
			if(substr($group_data_key,0,4) != 'psa_'){
				
				if($set_nulls)
					$group_data_value = null;
				
				$this->orm_data[$group_data_key] = $group_data_value;
			}
		}
	}


	/**
	 * Adds group tags.
	 * It manage {@link $tags} member array.
	 * You need to call {@link save()} method after to store tags to database.
	 * On error {@link Psa_Group_Exception} will be thrown.
	 * See examples and note in {@link Psa_Group} class description.
	 * {@internal This is wrapper method for add_remove_tag() method.}}
	 * 
	 * @param string|array $tag name of the tag or array with tag names.
	 * @return int 0 for failure, 1 for success
	 * @see remove_tag()
	 * {@internal @see add_remove_tag()  }}
	 */
	public function add_tag($tag){

		return $this->add_remove_tag($tag, 1);
	}


	/**
	 * Removes group tags.
	 * It manage {@link $tags} member array.
	 * You need to call {@link save()} method after to store tags to database.
	 * On error {@link Psa_Group_Exception} will be thrown.
	 * See examples and note in {@link Psa_Group} class description.
	 * {@internal This is wrapper method for add_remove_tag() method.}}
	 * 
	 * @param string|array $tag name of the tag or array with tag names. <kbd>'all'</kbd> to remove all tags.
	 * @return int 0 for failure, 1 for success
	 * @see add_tag()
	 * {@internal @see add_remove_tag()  }}
	 */
	public function remove_tag($tag){

		return $this->add_remove_tag($tag, 0);
	}


	/**
	 * Adds or removes group tags.
	 * This method is called from {@link remove_tag()} and {@link add_tag()} methods.
	 * It throws {@link Psa_Group_Exception} on error.
	 * 
	 * @param string|array $tag id of the tag or array with tag ids.
	 * @param int $action 1 add, 0 remove
	 * @return int 0 for failure, 1 for success
	 * @see remove_tag()
	 * @see add_tag()
	 * @throws Psa_Group_Exception
	 * @ignore
	 */
	protected function add_remove_tag($tag, $action){

		if($tag){
			
			// global config array
			global $PSA_CFG;
			
			// if $tag is not array make it array for foreach loop
			if(!is_array($tag))
				$tag = array($tag);
				
			// add or remove tag
			foreach($tag as $tag_key => &$tag_value){
				
				// add tag. Add value 1 to $this->tags array for index $tag
				if($action == 1){
					
					// add all tags
					if($tag_value == 'all'){
						$tag_value = 'psa_all';
					}
					
					$this->tags[$tag_value] = 1;
				}

				// remove tag
				else if($action == 0){
					
					// remove all tags
					if($tag_value == 'all'){
						$this->tags = array();
						break;
					}
					// remove specific tag
					else
						unset($this->tags[$tag_value]);
				}
			}
			
			// write log
			if($PSA_CFG['logging']['max_log_level'] >= 2){
				$this->log("Group tag changed: " . implode(',', $tag) . " action=" . ($action ? 'add' : 'remove'), __METHOD__, 2);
			}
			
			return 1;
		}
		
		include_once 'Psa_Group_Exception.class.php';
		throw new Psa_Group_Exception('Error changing group tags. Invalid $tag method parameter', 36);
	}


	/**
	 * Unsets some properties from group object that we do not want to save in the database.
	 * This is helper function for {@link restore()} and save() functions.
	 * 
	 * @param object $group_object
	 * @ignore
	 */
	protected function serialize_clean_obj($group_object){
		
		if(is_object($group_object)){
			unset($group_object->orm_data, $group_object->orm_data_changed, $group_object->id, $group_object->name, $group_object->psa_database, $group_object->new_group, $group_object->exists);
		}
	}


	/**
	 * Puts user in the group or more users if the <var>$user_id</var> argument is array.
	 * Group membership changes are immediately stored in the database and you don't have to 
	 * call {@link save()} method after.
	 * If you creating a new group this method must be called after {@link save()} method.
	 * Throws {@link Psa_Group_Exception} on error.
	 * 
	 * Example:
	 * <code>
	 * // group object
	 * $group = new Psa_Group('my_user_group');
	 * // put users with IDs 1,3 and 6 into the group
	 * $group->add_user(array(1,3,6));
	 * </code>
	 * 
	 * {@internal This is wrapper method for add_remove_user() method.}}
	 * @param int|array $user_id id of the user. Or array with user ids.
	 * @return int 1 for success, -1 user already was in the group (or more groups) 
	 * or user does not exists
	 * @see remove_user()
	 * @throws Psa_Group_Exception
	 * {@internal @see add_remove_user()}}
	 */
	public function add_user($user_id){
		return $this->add_remove_user($user_id, 1);
	}
	
	
	/**
	 * Removes user from the group or more users if the <var>$user_id</var> argument is array.
	 * Group membership changes are immediately stored in the database and you don't have to 
	 * call {@link save()} method after.
	 * If you creating a new group this method must be called after {@link save()} method.
	 * Throws {@link Psa_Group_Exception} on error.
	 * 
	 * Example:
	 * <code>
	 * // group object
	 * $group = new Psa_Group('my_user_group');
	 * // remove user with ID 6 from the group
	 * $group->remove_user(6);
	 * </code>
	 * 
	 * {@internal This is wrapper method for add_remove_user() method.}}
	 * @param int|array $user_id id of the user. Or array with user ids.
	 * @return int 1 for success, -1 user already was in the group (or more groups) 
	 * or user does not exists
	 * @see remove_user()
	 * @throws Psa_Group_Exception
	 * {@internal @see add_remove_user()}}
	 */
	public function remove_user($user_id){
		return $this->add_remove_user($user_id, 0);
	}
	
	
	/**
	 * Puts user in the group or removes user from group (or more users if $user_id argument is array).
	 * This method is called from add_user() and remove_user() methods.
	 * 
	 * @param int|array|string $user_id id of the user or array with user ids.
	 * @param int $action 1 add, 0 remove
	 * @return int 1 for success, -1
	 * @see add_user()
	 * @see remove_user()
	 * @throws Psa_Group_Exception
	 * @ignore 
	 */
	protected function add_remove_user($user_id, $action){
		
		if($this->exists && $user_id){
			
			// global config array
			global $PSA_CFG;
			
			// if $user_id is not array make it array for foreach loop
			if(!is_array($user_id))
				$user_id = array($user_id);
			
			// flag if some query failed in the foreach loop
			$success = $failed = 0;
				
			// add one or more users to group or remove from group
			foreach ($user_id as $user_id_key => &$user_id_value){
				
				// add to group
				if($action == 1){
					$sql = "INSERT INTO {$PSA_CFG['database']['table']['user_in_group']} (group_id, user_id) VALUES ('{$this->id}', '{$user_id_value}')";
				}
				// remove from group
				else if($action == 0){
					
					// remove all users from group
					if($user_id_value == 'all')
						$sql = "DELETE FROM {$PSA_CFG['database']['table']['user_in_group']} WHERE group_id = '{$this->id}'";
					// remove specific user from group
					else
						$sql = "DELETE FROM {$PSA_CFG['database']['table']['user_in_group']} WHERE group_id = '{$this->id}' AND user_id='{$user_id_value}'";
				}
				
				// run query against the database
				try{
					$this->psa_database->query($sql);
				}
				catch(Psa_Db_Exception $e){
					$failed = 1;
				}
				
				// if no rows affected user already wasn't or was in the group depending on $action
				if($this->psa_database->affected_rows() <= 0)
					$failed = 1;
				else{
					$success = 1;
				}
			}
						
			if($success){
				
				// write log
				if($PSA_CFG['logging']['max_log_level'] >= 2)
					$this->log("Members of group changed: " . implode(',', $user_id) . " action=" . ($action ? 'add' : 'remove'), __METHOD__, 2);
				
				if(!$failed)
					return 1;
			}
			
			return -1;
		}
		
		include_once 'Psa_Group_Exception.class.php';
		throw new Psa_Group_Exception('Error changing members of group. Group not exists or invalid $user_id method parameter.', 22);
	}	
	
	
	/**
	 * Handles logging for this class.
	 * It calls log() method of Psa_Logger object.
	 * 
	 * @param string $message log message
	 * @param string $method class method which writes log message
	 * @param int $level log level
	 * @param string $type 'general', 'error', 'warning' ...
	 * @see Psa_Logger::log()
	 * @ignore
	 */
	protected function log($message, $method = '', $level = 1, $type = ''){

		// global config array
		global $PSA_CFG;
		
		// if logging is enabled
		if($PSA_CFG['logging']['max_log_level'] >= $level){
			
			// parameters for Psa_Logger::log() method
			$log_data['group_id'] = $this->id;
			$log_data['groupname'] = $this->name;
			$log_data['message'] = $message;
			$log_data['function'] = $method;
			$log_data['level'] = $level;
			$log_data['type'] = $type;
			Psa_Logger::get_instance()->log($log_data);
		}
	}


	/**
	 * @ignore
	 */
	public function __set($name, $value) {
		
		// check if orm property should be set
		if(array_key_exists($name,$this->orm_data)){
			$this->orm_data[$name] = $value;
			$this->orm_data_changed[$name] = &$this->orm_data[$name];
		}
		// if some property is not set set it
		else if(!isset($this->$name)){
			$this->$name = $value;
		}
		// show error
		else{
			$trace = debug_backtrace();
			trigger_error('Cannot set protected property: ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_ERROR);
			return null;
		}
	}
	
	
	/**
	 * @ignore
	 */
	public function __get($name){
		
		// enable to get orm properties
		if (array_key_exists($name, $this->orm_data)) {
			return $this->orm_data[$name];
		}
		
		// Makes some properties read-only
		if($name == 'tags' or $name == 'id' or $name == 'name' or $name == 'exists'){
			return $this->$name;
		}
		
		$trace = debug_backtrace();
		trigger_error('Undefined object property: ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
		return null;
	}
	
	
	/**
	 * @ignore
	 */
	public function __isset($name) {
		
		if(isset($this->$name) or isset($this->orm_data[$name]))
			return true;
			
		return false;
	}

}

